using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Magiq.Support;
using Magiq.Support.DynamicClasses;

namespace Magiq.Databases
{
    public abstract class DynamicSelectCommandBuilder : CommandBuilder
    {
        private readonly List<string> calculatedColumns = new List<string>();
        private readonly ClassFactory classFactory = ClassFactory.Instance;
        private readonly List<string> constantColumns = new List<string>();
        private readonly List<object> constants = new List<object>();
        private readonly List<string> constantSqlValues = new List<string>();
        private readonly Dictionary<ColumnsAssignment, List<string>> queryDynamicClassMapping;
        private IEnumerable<string> calculatedSqlValues;
        private readonly IEnumerable<ColumnsAssignment> assignments;
        protected string[] BaseSelectedValues { get; set; }
        protected Type DynamicClass { get; private set; }
        protected IList<MemberBinding> Bindings { get; private set; }
        protected ParameterExpression Parameter { get; private set; }
        protected string[] Columns { get; set; }

        protected DynamicSelectCommandBuilder(IDatabaseAdapter databaseAdapter, IQueryable where,
                                              IEnumerable<ColumnsAssignment> assignments, bool addTypeReference)
            : base(databaseAdapter, where, addTypeReference)
        {
            queryDynamicClassMapping = new Dictionary<ColumnsAssignment, List<string>>();
            Bindings = new List<MemberBinding>();
            this.assignments = assignments;
        }

        protected override void Init(Type elementType)
        {
            base.Init(elementType);
            Parameter = Expression.Parameter(Where.ElementType, "x");

            BuildDynamicClassForSelect();
            OnDynamicClassForSelectBuilt();
            ProcessColumnAssignments();
            ProcessDynamicSelect();
            ProcessConstantValues();

            Columns = calculatedColumns.Union(constantColumns).ToArray();
            BaseSelectedValues = calculatedSqlValues.Union(constantSqlValues).ToArray();
        }

        protected virtual void OnDynamicClassForSelectBuilt()
        {
        }

        protected virtual List<DynamicProperty> CreateDynamicProperties()
        {
            var properties = new List<DynamicProperty>();
            foreach (var assignment in assignments)
            {
                var propertyTypeMapping = DatabaseAdapter.GetMappingOf(assignment.PropertyType);

                var list = new List<string>();
                if (propertyTypeMapping.IsEntity)
                {
                    foreach (var id in propertyTypeMapping.Ids)
                    {
                        var name = assignment.PropertyName + "_" + id.PropertyName;
                        properties.Add(new DynamicProperty(name, id.Type));
                        list.Add(name);
                    }
                }
                else
                {
                    properties.Add(new DynamicProperty(assignment.PropertyName, assignment.ValueType));
                    list.Add(assignment.PropertyName);
                }
                queryDynamicClassMapping.Add(assignment, list);
            }
            return properties;
        }

        private void BuildDynamicClassForSelect()
        {
            var properties = CreateDynamicProperties();
            DynamicClass = classFactory.GetDynamicClass(properties);
        }

        private void ProcessConstantValues()
        {
            foreach (var constant in constants)
            {
                if (constant == null)
                {
                    constantSqlValues.Add("null");
                }
                else
                {
                    var p = Command.CreateParameter();

                    p.ParameterName = "@p" + Command.Parameters.Count;
                    p.Value = constant;
                    Command.Parameters.Add(p);
                    constantSqlValues.Add(p.ParameterName);
                }
            }
        }

        private void ProcessDynamicSelect()
        {
            var newExpression = Expression.MemberInit(Expression.New(DynamicClass), Bindings);

            var lambda = Expression.Lambda(newExpression, Parameter);

            var selectQuery = Where.Provider
                .CreateQuery(Expression.Call(typeof (Queryable),
                                             "Select",
                                             new[] {Where.ElementType, lambda.Body.Type},
                                             Where.Expression,
                                             Expression.Quote(lambda)));

            SetCommand(DatabaseAdapter.GetCommand(selectQuery));
            calculatedSqlValues = GetCalculatedSqlValues();
        }


        protected virtual IEnumerable<string> GetCalculatedSqlValues()
        {
            if (calculatedColumns.IsEmpty())
                return new List<string>();

            var parser = new SelectColumnExpressionParser();
            return parser.Parse(Command.CommandText.GetFrom("SELECT ").GetUntil("\r\nFROM "))
                .Select(x => x.GetUntil(" AS ").Trim());
        }

        private void ProcessColumnAssignments()
        {
            foreach (var columnAssignment in assignments)
            {
                if (columnAssignment.ValueExpression.IsConstant())
                {
                    foreach (var constant in GetConstantsFrom(columnAssignment.ValueExpression))
                        constants.Add(constant);

                    constantColumns.AddRange(columnAssignment.Columns);
                }
                else
                {
                    var dynamicProperties = queryDynamicClassMapping[columnAssignment];
                    var expressions = GetSelectExpressionsFrom(columnAssignment.ValueExpression).ToArray();
                    for (var i = 0; i < expressions.Length; i++)
                    {
                        var expression = expressions[i];
                        var lambda = Expression.Lambda(expression, expression.GetParameter()).ChangeParameters(Parameter).Body;
                        Bindings.Add(Expression.Bind(DynamicClass.GetProperty(dynamicProperties[i]), lambda));
                    }

                    calculatedColumns.AddRange(columnAssignment.Columns);
                }
            }
        }

        private IEnumerable<object> GetConstantsFrom(LambdaExpression expression)
        {
            var valueMapping = DatabaseAdapter.GetMappingOf(expression.Body.Type);
            var parameters = new object[expression.Parameters.Count];
            var value = expression.Compile().DynamicInvoke(parameters);

            if (valueMapping.IsEntity)
            {
                foreach (var id in valueMapping.Ids)
                {
                    if (Equals(value, null))
                        yield return null;
                    else
                        yield return value.Property(id.PropertyName);
                }
            }
            else
            {
                yield return value;
            }
        }

        private IEnumerable<Expression> GetSelectExpressionsFrom(LambdaExpression expression)
        {
            var valueMapping = DatabaseAdapter.GetMappingOf(expression.Body.Type);

            if (valueMapping.IsEntity)
            {
                foreach (var id in valueMapping.Ids)
                {
                    var property = Expression.Property(expression.Body, id.PropertyName);
                    yield return property;
                }
            }
            else
            {
                yield return expression.Body;
            }
        }
    }
}